feat: emit tax_lot_v1 event for tax-lot reconstruction - #693
Merged
thlpkee20-wq merged 2 commits intoJul 29, 2026
Conversation
|
@Muyideen-js Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PR: Emit
tax_lot_v1Event for Tax-Lot ReconstructionCloses #536
Summary
Indexers need a stable per-holder per-bucket event topic to reconstruct tax lots off-chain. This PR adds a
tax_lt1event emitted on every tax-bucket update duringclaim(), providing indexers with the exact decomposition of each payout into return of capital (non-taxable) and capital gains (taxable), along with the associated period and ledger timestamp.Motivation
Off-chain tax-lot accounting requires knowing, for each holder payout, how much was return of capital (cost basis recovery) versus capital gains (taxable income). Without a dedicated event, indexers would need to replay the entire
rollover_distributionlogic to reconstruct this decomposition, which is fragile and computationally expensive.The new
tax_lt1event makes this decomposition observable directly from the event stream — indexers simply subscribe totopics[0] == "tax_lt1"and get the exact per-claim breakdown.New Event Schema
tax_lt1(version 1)Topic:
(tax_lt1, issuer, namespace, token)Data tuple (field order for indexer deserialization):
holderAddressreturn_of_capitali128capital_gainsi128amounti128return_of_capital + capital_gains)period_idu64timestampu64Invariant:
return_of_capital + capital_gains == amount(lossless decomposition).Emitted by:
rollover_distribution()insrc/tax_bucket.rs, called duringclaim().Frequency: One event per successful
claim()call with a positive payout.Architecture
Before
The existing
rollover_distributiononly emittedtax_rollin one branch (whenremaining_basis < amount). There was no per-claim observable decomposition, making tax-lot reconstruction impossible from event data alone.After
The existing
tax_rollevent is preserved for backward compatibility. The newtax_lt1event is additive and emitted unconditionally on every tax-bucket update.Changes Made
src/tax_bucket.rs— Core event emissionEVENT_TAX_LOT_V1constantsymbol_short!("tax_lt1")with full doc-comment documenting topic, data tuple, and field orderrollover_distributionsignatureperiod_id: u64andtimestamp: u64parameterstax_lt1publish after every bucket update, before returningTaxBucketResultsrc/lib.rs— Call site updateprevious_period_id.expect(...)andnow(ledger timestamp) torollover_distributionsrc/test_indexer_fixtures.rs— Tests (5 new tests)fixture_tax_lot_v1_topic_symbol_is_stableEVENT_TAX_LOT_V1constant equalssymbol_short!("tax_lt1")— pins the symbol against accidental renamefixture_tax_lot_v1_data_tuple_shapereturn_of_capital > 0andreturn_of_capital + capital_gains == amountfixture_tax_lot_v1_capital_gains_when_basis_exhaustedremaining_basis < amount,capital_gains > 0andreturn_of_capitalequals the remaining basisfixture_tax_lot_v1_zero_payout_emits_no_eventshare_bps = 0fails withNoPendingClaims; notax_lt1event is emittedfixture_tax_lot_v1_burst_emits_n_eventstax_lt1eventsdocs/tax-lot-export-event.md— DocumentationFull specification of the
tax_lt1event schema, field order, security invariants, and indexer integration guide.Test Coverage
Edge Case: Empty Period — Zero Events
Test:
fixture_tax_lot_v1_zero_payout_emits_no_eventEdge Case: Burst Period — N Events
Test:
fixture_tax_lot_v1_burst_emits_n_eventsCapital Gains Coverage
Test:
fixture_tax_lot_v1_capital_gains_when_basis_exhaustedSecurity Considerations
Event guard on positive payout:
tax_lt1is only emitted whentotal_payout > 0. Failed claims (zero share, blacklisted, delay not elapsed) never emit the event, preventing spurious events.Lossless decomposition: The invariant
return_of_capital + capital_gains == amountis asserted in tests, ensuring the decomposition is always complete.Tamper-resistant timestamps:
timestampis sourced fromenv.ledger().timestamp(), which is the authoritative Soroban ledger timestamp — not caller-supplied.Period integrity:
period_idis sourced from the claimed period's on-chain entry, not from caller input.Symbol distinctness: The
tax_lt1symbol is distinct from all other event symbols. The existingv2_event_symbols_are_all_distincttest picks up the new symbol (via its test list, which would need the new symbol added if it were anev_idx2event type — as a standalone event it has no collision risk).Existing tax_roll preserved: The pre-existing
EVENT_TAX_ROLLOVERevent is still emitted on capital gains, ensuring backward compatibility with any indexers consuming that event.Backward Compatibility
EVENT_TAX_ROLLOVERcontinues to be emitted with the same schema.tax_lt1event is purely additive — existing indexers that ignore unknown topics are unaffected.rollover_distributionsignature changed (addedperiod_idandtimestamp), but this is apub fnonly used internally — no external callers.STORAGE_LAYOUT_VERSIONis not bumped.Files Changed
src/tax_bucket.rssrc/lib.rssrc/test_indexer_fixtures.rsdocs/tax-lot-export-event.mdChecklist
EVENT_TAX_LOT_V1constant defined and documentedtax_lt1event emitted unconditionally on every tax-bucket update(holder, return_of_capital, capital_gains, amount, period_id, timestamp)rollover_distributioncall site updated withperiod_idandtimestampremaining_basis < amountpathroc + cg == amount) testedtax_rollevent preservedExample Commit Message
How to Test
Expected: All 5 new tests pass alongside the existing 480+ test suite.